# Copyright (C) 2005-2006 Frederic Back <fredericback@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.

import os
import sys
import string
import threading
import time
import imp

from _guitools import createLabel, parseGimpPalette, errorMessage

#--------------------------------------------------------------------------------
# try to import libraries

try: from GeoTypes import Box, Point
except:
    errorMessage(_("Library not found"), "Please install the GeoTypes library.")

try: import psycopg
except: 
    try: import psycopg2.psycopg1 as psycopg
    except:
        errorMessage(_("Library not found"), "Please install the psycopg library.")

#--------------------------------------------------------------------------------

import gtk
import unicodedata

from _dbtools import *
from _geodata import ResultSet, Layer, LayerStyle, Project
#from _project_io import Loader, Saver
from _guiwindows import *

from _pane_layer import LayerView
from _pane_db import DatabasePane
from _options import Options
from _queryeditor import QueryEditor
from _history import HistoryView
from _options import save_options_to_configfile

import _i18n

try: import subprocess
except: pass

def help(w):
    if not os.access(Options.docPath,os.R_OK):
        print "Could not access documentation:"
        print Options.docPath
        return
    try: pid = Popen(["yelp",Options.docPath]).pid
    except: pid = os.spawnlp(os.P_NOWAIT, "yelp", "yelp", Options.docPath)

def about(w):
    d = gtk.AboutDialog()
    d.set_name("mezoGIS")

    c = unicodedata.lookup('COPYRIGHT SIGN')
    e = unicodedata.lookup('LATIN SMALL LETTER E WITH ACUTE')
    d.set_copyright(u"Copyright "+c+" 2005-2006 Fr"+e+"d"+e+"ric Back")
    d.set_website("http://www.mezogis.org")
    d.set_comments(_("mezoGIS is a lightweight GIS application"))
    d.run()
    d.destroy()

ui = """
<ui>

    <menubar name="MenuBar">
        <menu action="Project">
            <placeholder name="LoadAndSave" />
            <separator />
            <menuitem action="Quit"/>
        </menu>
        <menu action="Database">
            <menuitem action="Connect"/>
            <menuitem action="ReloadTables"/>
            <separator />
            <placeholder name="DatabasePlaceholder" />
            <separator />
            <menuitem action="QueryEditor"/>
        </menu>
        <menu action="Plugins">
            <placeholder name="LoadedPlugins"/>
        </menu>
        <menu action="Tools">
            <placeholder name="Tools_1"/>
        </menu>
        <menu action="Help">
            <menuitem action="About"/>
        </menu>
    </menubar>
    
    <toolbar name="Toolbar">
      <placeholder name="SQLquery">
      </placeholder>
    </toolbar>
    
</ui>"""

def getPGButler():
    return PGButler.instance

class PGButler:
    """ Singleton containing the mezoGIS GUI.
        Use PGButler.instance to access it.
        It gets initialised at the end of this file.
        
        Most class attributes are not wrapped by variables.
        You should access them directly for now.
    """
    
    instance = None # singleton instance

    def __init__(self):
        if PGButler.instance is not None:
            raise Exception, "PGButler may only be instanciated once. Use PGButler.instance instead." 
        PGButler.instance = self
        self.extensions = []


    def die(self):
        # stop drawing
        try:
            for t in MapWindow.instances:
                t.canvas.stop()
        except: pass

        # close db connections
        try: self.db.handle.close()
        except: pass

        # save configuration
        self.paneHistory.saveHistory(Options.configpath+"history")
        save_options_to_configfile()

    def register_extension(self, extension):
        self.extensions.append(extension)

    def connect(self):
        """ Connect to a database. """
        conid = self.statusbar.get_context_id("connection")
        self.statusbar.pop(conid)
        try:
            self.db.connect()
            for widget in self.sensitiveWhileConnected: widget.set_sensitive(True)
            self.statusbar.push(conid,_("connection succeeded"))            
        except psycopg.OperationalError, e:
            
            for widget in self.sensitiveWhileConnected: widget.set_sensitive(False)
            self.statusbar.push(conid,_("Connection failed: %s")%str(e).splitlines()[0])
        
        # poll information about this connection in the background
        self.reloadTableinfo()


    def run(self):
        """ Create the main GUI element for mezoGIS """
        
        self.connected = False
        
        # setup gui. 
        self.__setup_main_window()

        # list of widgets to make sensitive only while connected to a database
        self.sensitiveWhileConnected = [self.ag_connected,self.paneDbinfo]
       
        # try to connect to database
        self.db = Database()
        self.db.host = Options.default_host
        self.db.user = Options.default_user
        self.db.pw = Options.default_pw
        self.db.dbname = Options.default_dbname
        self.db.port = Options.default_port
        self.connect()

        # load default palettes
        LayerStyle.defaultFillPalette = parseGimpPalette(Options.palettePath+"fill.palette")
        LayerStyle.defaultLinePalette = parseGimpPalette(Options.palettePath+"line.palette")

        # load history
        self.paneHistory.loadHistory(Options.configpath+"history")
        
        for extension in self.extensions:
            extension()


    def __setup_main_window(self):
        self.win = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.win.set_title("mezoGIS %s" %Options.version)
        self.win.set_size_request(Options.sizeX, Options.sizeY)
        self.win.connect("delete_event", self.cb_quitSignal)

        # set window manager icons
        icon128 = gtk.gdk.pixbuf_new_from_file(Options.iconpath+"mezogis_128.png")
        icon64 = gtk.gdk.pixbuf_new_from_file(Options.iconpath+"mezogis_64.png")
        icon48 = gtk.gdk.pixbuf_new_from_file(Options.iconpath+"mezogis_48.png")
        icon32 = gtk.gdk.pixbuf_new_from_file(Options.iconpath+"mezogis_32.png")
        icon16 = gtk.gdk.pixbuf_new_from_file(Options.iconpath+"mezogis_16.png")
        self.win.set_icon_list(icon128,icon64,icon48,icon32,icon16)
    
        self.toplevelVbox = toplevelVbox = gtk.VBox(False, 0)
        self.win.add(toplevelVbox)
        self.win.realize()
        
        # create menu bar
        toplevelVbox.pack_start(self.__setup_menubar(),False,False)

        # create side pane
        self.paneDbinfo = DatabasePane(self)
        self.layerView = LayerView(self)
        self.sidepane = SidePane()
        self.sidepane.addWidget(self.paneDbinfo,_("Tables in Database"),"Database",gtk.STOCK_NETWORK)
        self.sidepane.addWidget(self.layerView,_("Maps and Layers"),"Project",gtk.STOCK_FILE)
        toplevelVbox.pack_start(self.sidepane.widget)

        # create status bar
        self.statusbar = gtk.Statusbar()
        toplevelVbox.pack_start(self.statusbar,False)

        # show self
        self.win.show_all()

        # create a query editor
        self.queryEditor = QueryEditor()
        self.queryEditor.connect("delete_event", self.queryEditor.hide_on_delete)
        self.queryEditor.okButton.connect("clicked", self.__goButton)

        # add history pane to the query editor
        self.paneHistory = HistoryView()
        i = self.queryEditor.notebook.append_page(self.paneHistory,gtk.Label(_("History")))
        self.paneHistory.connect("activated",lambda w,command: self.setQueryBuffer(command))


    def __setup_menubar(self):
        uimanager = gtk.UIManager()
        self.uimanager = uimanager
        self.accelgroup = uimanager.get_accel_group()
        self.win.add_accel_group(self.accelgroup)

        # always visible actions
        
        ag_global = gtk.ActionGroup('ag_global')
        ag_global.add_actions([
            ('Project', None, _i18n.msg["Project"]),

            ('Close', gtk.STOCK_CLOSE, _i18n.msg["Close"], None, _i18n.msg["Close"].replace("_",""), self.cb_closeSignal),
            ('Quit', gtk.STOCK_QUIT, _i18n.msg["Quit"], None, _i18n.msg["Quit"].replace("_",""), self.cb_quitSignal),

            ('Database', None, _i18n.msg["Database"]),
            ('Connect', gtk.STOCK_NETWORK, _i18n.msg["Connect"], None,_i18n.msg["Connect"].replace("_",""), self.connect_dialog),

            ('Plugins', None, _i18n.msg["Plugins"]),
            ('Tools', None, _i18n.msg["Tools"]),
            
            ('Help', None, _i18n.msg["Help"]),
           #('Contents', gtk.STOCK_HELP, _i18n.msg["Contents"], "F1", _i18n.msg["Contents"].replace("_",""), help ),
            ('About', gtk.STOCK_ABOUT, _i18n.msg["About"], None, _i18n.msg["About"].replace("_",""), about ),
        ])
        # only visible while connected to a database
        self.ag_connected = gtk.ActionGroup('ag_connected')
        self.ag_connected.add_actions([
            ('ReloadTables', gtk.STOCK_REFRESH, _i18n.msg["ReloadTables"], None, _i18n.msg["ReloadTables"].replace("_",""), self.reloadTableinfo),
            ('QueryEditor', gtk.STOCK_EDIT, _i18n.msg["QueryEditor"], "<CONTROL>e", _i18n.msg["QueryEditor"].replace("_",""), lambda w: self.queryEditor.present())
        ])

        uimanager.insert_action_group(ag_global, 0)
        uimanager.insert_action_group(self.ag_connected, 0)
        uimanager.add_ui_from_string(ui)

        menubar = uimanager.get_widget('/MenuBar')
        menubar.show()

        return menubar


    def getQueryBuffer(self):
        b = self.queryEditor.textbuffer
        return b.get_text(b.get_start_iter(),b.get_end_iter())

    def setQueryBuffer(self, query):
        ''' Set the current sql command '''
        self.queryEditor.setQuery(query)
        self.queryEditor.present()

    def reloadTableinfo(self, widget=None, data=None):
        self.paneDbinfo.refresh( self.db )

    def __goButton(self, widget):
        if self.paneDbinfo.info is None: return
        query = self.getQueryBuffer()
        if self.runQuery(query) == True:
            self.paneHistory.addCommand(query)

    def runQuery(self, query, layer = None, resultset = None):
        """ Send an sql query to the database.

        'layer' is an instance of Layer in which the result data will be placed.
        If 'layer' is None, a new Layer will be created.
        
        If 'resultset' is set, this new layer will be placed inside it.

        Parse the query first and try to find out if the result will include geometry.
        Find out which table this geometry comes from.
        """

        if query == "": return
        db = self.db

        # make sure we have finished polling table information
        if self.paneDbinfo.info is None:
            print "info not ready"
            return
        if not self.paneDbinfo.info.finished:
            if not self.paneDbinfo.info.finished:
                print "info not finished"
                return False

        # parse query for table names
        # we start with longer strings to avoid this: tmp is in tmp_crossroad
        tablelist = []
        q = query
        for table in self.paneDbinfo.info.getSortedByNameLength():
            #print table[0]
            if q.find(table.name) > -1:
                tablelist.append(table)
                q = q.replace(table.name,"") # remove from q

        if(db.connected == False):
            d = gtk.MessageDialog(self.win.get_toplevel(),
                gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
                gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
                _("Not connected to database") )
            d.run()
            d.destroy()
            return False

        # create a new cursor
        cursor = db.handle.cursor()

        # execute the query, catch sql syntax errors
        try: cursor.execute(query)
        except psycopg.ProgrammingError, msg:

            if query == self.queryEditor.getQuery():
                qe = self.queryEditor
                qe.present()
            else:
                qe = QueryEditor()
                qe.setQuery(query)

            qe.markError( str(msg) )

            # hack
            lines = str(msg).splitlines()
            msg = lines[0]
            
            # create a popup dialog with a description of the error
            d = gtk.MessageDialog(self.win.get_toplevel(),
                gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
                gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
                _("SQL Error") )
            d.set_resizable(True)
            d.format_secondary_text(msg)
            textview = gtk.TextView(qe.textbuffer)
            textview.set_editable(False)
            sw = gtk.ScrolledWindow()
            sw.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC)
            sw.set_shadow_type(gtk.SHADOW_IN)
            sw.add(textview)
            d.vbox.pack_start(sw)
            #d.set_size_request(300,-1)
            d.show_all()
            d.run()
            d.destroy()

            try: self.db.handle.rollback()
            except psycopg.Error, e:
                print "Could not roll back connection:"
                print e

            return False
        except psycopg.Error, e:
            self.db.handle.rollback()
            d = gtk.MessageDialog(self.win.get_toplevel(),
                gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
                gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
                "Error" )
            d.format_secondary_text(str(e))
            d.run()
            d.destroy()
            return False

        db.handle.commit()

        # display result set
        if cursor.description != None:

            # create a new layer
            if layer is None: layer = Layer()
            self.showResults(cursor, query, layer, resultset, tablelist)

        # if query contains command that might have modified the table extents,
        # update table information
        found = 0
        s = query.upper()
        for token in ('UPDATE','INSERT','DROP','DELETE','CREATE','VACUUM ANALYSE'):
            if s.find(token) > -1: found += 1
        if found > 0: self.reloadTableinfo()

        return True


    def showResults(self, cursor, query, layer, resultset, tablelist = None):
        """ Open a new mapwindow or a tablewindow. """

        if tablelist == None: tablelist = []
        layer.setDataSource(cursor, query, tablelist)
        
        # open a table view if the layer contains no geometry, and a map view if it does.
        if len(layer.geomCols) == 0: openTableView(layer)
        else: 
             # This is a new layer, it is not yet in a resultset.
            if layer.resultset == None:
            
                # create a new resultset if none is given
                if resultset == None:
                    result = ResultSet()
                    result.addLayer(layer)
                    self.layerView.getProject().addResultset(result)
                else:
                    resultset.addLayer(layer)
                
                # open a mapview
                mapview = openMapView(layer.resultset)

                # Check if the layer's extent could be computed correctly. If not,
                # redraw the map as soon as an extent is available.
                # This is a very gentle hack for special cases, like for
                # instance: "SELECT * FROM MakePoint(0,0),MakePoint(1,0)"
                if layer.extent is None:
                    layer.connect("extent-computed", lambda w,m: m.refreshMap(), mapview)

            else:
                pass

        # start loading
        layer.loadData()

    # ask for connection data and atempt connection
    def connect_dialog(self, widget, event=None, data=None):
        dialog = gtk.Dialog(_('Database Connection'),
            self.win.get_toplevel(),
            gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
            (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, 
            gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
        dialog.set_has_separator(False)
        dialog.set_position(gtk.WIN_POS_CENTER)

        table = gtk.Table(5,2,False)
        dialog.vbox.pack_start(table)     
        
        host = gtk.Entry()
        host.set_text(self.db.host)
        table.attach(createLabel(_("<b>host: </b>")), 0,1, 0,1)
        table.attach(host, 1,2, 0,1)

        user = gtk.Entry()
        user.set_text(self.db.user)
        table.attach(createLabel(_("<b>user: </b>")), 0,1, 1,2) 
        table.attach(user, 1,2, 1,2)

        pw = gtk.Entry()
        pw.set_text(self.db.pw)
        table.attach(createLabel(_("<b>password: </b>")), 0,1, 2,3) 
        table.attach(pw, 1,2, 2,3)
        pw.set_visibility(False)

        dbname = gtk.Entry()
        dbname.set_text(self.db.dbname)
        table.attach(createLabel(_("<b>database: </b>")), 0,1, 3,4) 
        table.attach(dbname, 1,2, 3,4)

        port = gtk.Entry()
        port.set_text(self.db.port)
        table.attach(createLabel(_("<b>port: </b>")), 0,1, 4,5) 
        table.attach(port, 1,2, 4,5)

        save = gtk.CheckButton(_("Save info in plain text"))
        dialog.vbox.pack_start(save)
        dialog.vbox.show_all()

        if(dialog.run() == gtk.RESPONSE_ACCEPT):
            self.db.host = host.get_text()
            self.db.user = user.get_text()
            self.db.pw = pw.get_text()
            self.db.dbname = dbname.get_text()
            self.db.port = port.get_text()
            self.connect()

        if(save.get_active()):
            Options.default_db = self.db.host
            Options.default_user = self.db.user
            Options.default_pw = self.db.pw
            Options.default_dbname = self.db.dbname
            Options.default_port = self.db.port

        dialog.destroy()
        
    # close window and quit
    def cb_quitSignal(self, widget=None, event=None, data=None):
        if self.win.has_toplevel_focus() or len(TableWindow.instances) == 0 or \
            len(MapWindow.instances) == 0:
            gtk.main_quit()
            return False
        else: self.cb_closeSignal(widget)
        return False

    def cb_closeSignal(self, w):
        # get active window and close it
        for t in TableWindow.instances + MapWindow.instances:
            if t.has_toplevel_focus(): return t.close()
                
        # if none active, close next
        for t in TableWindow.instances: return t.close()
        for t in MapWindow.instances: return t.close()
            




#---------------------------------------------------------------------------------------------------
class SidePane:
    ''' Side panel. Can aggregate more widgets. '''

    def __init__(self):

        self.pages = []
        self.widget = gtk.VBox()

        # icon, label, close
        self.icon = gtk.Image()
        h = gtk.HBox()
        h.set_border_width(6)
        h.pack_start(self.icon,False,False,5)
        self.label = gtk.Label("")
        self.label.set_alignment(0,0.5)
        h.pack_start(self.label)        
        self.closeButton = gtk.Button()
        i = gtk.Image()
        i.set_from_stock(gtk.STOCK_CLOSE,gtk.ICON_SIZE_MENU)
        self.closeButton.add(i)
        self.closeButton.set_relief(gtk.RELIEF_NONE)
        h.pack_end(self.closeButton,False)
        self.widget.pack_start(h,False,False)

        # notebook
        self.nb = gtk.Notebook()
        self.nb.set_tab_pos(gtk.POS_BOTTOM)
        self.nb.connect("switch-page",self.cb_pageswitch)
        self.widget.pack_start(self.nb)
        self.closeButton.set_no_show_all(True)
        self.widget.show_all()

    def addWidget(self, widget, label, shortlabel, stockicon):
        self.pages.append( (label,stockicon) )
        self.nb.append_page(widget, gtk.Label(shortlabel) )

    def cb_pageswitch(self, notebook, page, page_num):
        self.label.set_label( self.pages[page_num][0] )
        self.icon.set_from_stock( self.pages[page_num][1], gtk.ICON_SIZE_MENU )


#----------------------------------------------------- create singleton instance
PGButler()
